package com.halasa.backup;

import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.io.FileUtils;

public class IncrementalBackup {
    
    private static final Logger LOG = Logger.getLogger(IncrementalBackup.class.getSimpleName());

    private FileFilter fileFilter;
    private FileFilter dirFilter;
    
    private File sourceRoot;
    private File backupRoot;
    private File incBackupRoot;
    
    private int created = 0;
    private int updated = 0;
    private int removed = 0;
    
    private Config config;
    
    public IncrementalBackup(Config config) {
        
        this.config = config;
        
        this.sourceRoot = config.getSourceDir();
        this.backupRoot = new File(config.getBackupBaseDir(), "image");
        if (! this.backupRoot.isDirectory()) {
            LOG.info("Creating backup image directory: '" + this.backupRoot + "'");
            this.backupRoot.mkdir();
        }
        
        DateFormat dateFormat = new SimpleDateFormat("yyMMdd");
        this.incBackupRoot = new File(config.getBackupBaseDir(), dateFormat.format(new Date()));
        if (! this.incBackupRoot.isDirectory()) {
            LOG.info("Creating inc backup directory: '" + this.incBackupRoot + "'");
            this.incBackupRoot.mkdir();
        }
        
        this.fileFilter = new FileFilter() {
            public boolean accept(File file) {
                return file.isFile();
            }
        };
        
        this.dirFilter = new FileFilter() {
            public boolean accept(File file) {
                return file.isDirectory();
            }
        };
    }
    
    public void run() throws IOException {
        backupDirectory(this.sourceRoot, this.backupRoot);
        removeOldIncDirs();
    }
    
    private void backupDirectory(File sourceDir, File backupDir) throws IOException {
        // Browse the source dir and find the modifications
        File[] sourceFileList = sourceDir.listFiles(this.fileFilter);
        for (int i = 0; i < sourceFileList.length; i++) {
            File sourceFile = sourceFileList[i];
            File backupFile = new File(backupDir, sourceFile.getName());
            if (backupFile.isFile()) {
                if (sourceFile.length() != backupFile.length() || sourceFile.lastModified() > backupFile.lastModified()) {
                    backupFile(sourceFile, backupFile);
                }
            }
            else {
                backupFile(sourceFile, backupFile);
            }
        }
        
        // Remove files and directories from the backup dir that do not exist in the source dir
        File[] backupFileList = backupDir.listFiles();
        for (int i = 0; i < backupFileList.length; i++) {
            File backupFile = backupFileList[i];
            File sourceFile = new File(sourceDir, backupFile.getName());
            if (! sourceFile.exists()) {
                remove(backupFile);
            }
        }
        
        // Recursivly process all subdirectories
        File[] sourceDirList = sourceDir.listFiles(this.dirFilter);
        for (int i = 0; i < sourceDirList.length; i++) {
            File sourceSubDir = sourceDirList[i];
            
            // Check if this directory should not be excluded
            String[] exclude = config.getExcludeDirs();
            if (exclude != null) {
                String absSubPath = getAbsSubPath(sourceSubDir.getAbsolutePath());
                boolean toBeExcluded = false;
                for (int excludeIndex = 0; excludeIndex < exclude.length; excludeIndex++) {
                    if (absSubPath.equals(exclude[excludeIndex])) {
                        toBeExcluded = true;
                        break;
                    }
                }
                if (toBeExcluded) {
                    if (LOG.isLoggable(Level.FINE)) {
                        LOG.fine("excluding dir: " + absSubPath);
                    }
                    continue;
                }
            }
            
            File backupSubDir = new File(backupDir, sourceSubDir.getName());
            if (! backupSubDir.isDirectory()) {
                backupSubDir.mkdir();
            }
            this.backupDirectory(sourceSubDir, backupSubDir);
        }
    }
    
    private void backupFile(File srcFile, File destFile) throws IOException {
        // First move the original to an incremental backup dir
        if (destFile.isFile()) {
            moveToIncBackupDir(destFile);
            updated++;
        }
        else {
            created++;
        }
        
        // Copy the source file to its destination
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("backup " + getAbsSubPath(srcFile.getAbsolutePath()));
        }
        boolean preserveFileDate = true;
        FileUtils.copyFile(srcFile, destFile, preserveFileDate);
    }
    
    private void remove(File backupFileOrDir) throws IOException {
        // Move the file/dir from backup dir to the incremental backup dir
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("remove " + getAbsSubPath(backupFileOrDir.getAbsolutePath()));
        }
        // If it is a directory, we need to count the number of its files
        if (backupFileOrDir.isDirectory()) {
            removed += 1 + countFilesInDirectory(backupFileOrDir);
        }
        else {
            removed++;
        }
        moveToIncBackupDir(backupFileOrDir);
    }
    
    private void moveToIncBackupDir(File backupFileOrDir) throws IOException {
        File incBackupDir = new File(this.incBackupRoot, getAbsSubPath(backupFileOrDir.getParent()));
        if (! incBackupDir.isDirectory()) {
            incBackupDir.mkdirs();
        }
        
        if (LOG.isLoggable(Level.FINER)) {
            LOG.finer("moving '" + backupFileOrDir + "' to '" + incBackupDir + "' ");
        }
        
        boolean createDestDir = false;
        if (backupFileOrDir.isDirectory()) {
            FileUtils.moveDirectoryToDirectory(backupFileOrDir, incBackupDir, createDestDir);
        }
        else {
            FileUtils.moveFileToDirectory(backupFileOrDir, incBackupDir, createDestDir);
        }
    }

    private String getAbsSubPath(String path) {
        String absSubpath = null;
        if (path.startsWith(this.sourceRoot.getAbsolutePath())) {
            absSubpath = path.substring(this.sourceRoot.getAbsolutePath().length());
        }
        else if (path.startsWith(this.backupRoot.getAbsolutePath())) {
            absSubpath = path.substring(this.backupRoot.getAbsolutePath().length());
        }
        return absSubpath;
    }
    
    private int countFilesInDirectory(File directory) {
        int count = 0;
        for (File file : directory.listFiles()) {
            if (file.isFile()) {
                count++;
            }
            if (file.isDirectory()) {
                count += countFilesInDirectory(file);
            }
        }
        return count;
    }
    
    public void removeOldIncDirs() {
        File[] incBackupDirs = config.getBackupBaseDir().listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return pathname.getName().matches("[12][0-9]{5}");
            }
        });
        
        if (incBackupDirs.length > config.getIncBackupCount()) {
            Arrays.sort(incBackupDirs, new Comparator<File>() {
                @Override
                public int compare(File file1, File file2) {
                    // The following order will put oldest dirs at the end of the array
                    return file2.getName().compareTo(file1.getName());
                }
            });
            for (int i = config.getIncBackupCount(); i < incBackupDirs.length; i++) {
                LOG.info("Removing old inc backup dir: " + incBackupDirs[i]);
                try {
                    FileUtils.deleteDirectory(incBackupDirs[i]);
                } catch (IOException e) {
                    LOG.log(Level.WARNING, "Could not remove inc backup dir '" + incBackupDirs[i].getAbsolutePath() + "'", e);
                }
            }
        }
    }

    public int getCreated() {
        return created;
    }
    public int getUpdated() {
        return updated;
    }
    public int getRemoved() {
        return removed;
    }
}
